/*
 * ====================================================================
 * Licensed to the Apache Software Foundation (ASF) under one
 * or more contributor license agreements.  See the NOTICE file
 * distributed with this work for additional information
 * regarding copyright ownership.  The ASF licenses this file
 * to you under the Apache License, Version 2.0 (the
 * "License"); you may not use this file except in compliance
 * with the License.  You may obtain a copy of the License at
 *
 *   http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing,
 * software distributed under the License is distributed on an
 * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
 * KIND, either express or implied.  See the License for the
 * specific language governing permissions and limitations
 * under the License.
 * ====================================================================
 *
 * This software consists of voluntary contributions made by many
 * individuals on behalf of the Apache Software Foundation.  For more
 * information on the Apache Software Foundation, please see
 * <http://www.apache.org/>.
 *
 */

package org.apache.http.entity.mime;

import org.apache.http.entity.mime.content.ContentBody;
import org.apache.http.util.Args;
import org.apache.http.util.ByteArrayBuffer;

import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.io.OutputStream;
import java.nio.ByteBuffer;
import java.nio.CharBuffer;
import java.nio.charset.Charset;
import java.util.List;

/**
 * HttpMultipart represents a collection of MIME multipart encoded content bodies. This class is
 * capable of operating either in the strict (RFC 822, RFC 2045, RFC 2046 compliant) or
 * the browser compatible modes.
 *
 * @since 4.3
 */
abstract class AbstractMultipartForm {

  private static ByteArrayBuffer encode(
    final Charset charset, final String string) {
    final ByteBuffer encoded = charset.encode(CharBuffer.wrap(string));
    final ByteArrayBuffer bab = new ByteArrayBuffer(encoded.remaining());
    bab.append(encoded.array(), encoded.position(), encoded.remaining());
    return bab;
  }

  private static void writeBytes(
    final ByteArrayBuffer b, final OutputStream out) throws IOException {
    out.write(b.buffer(), 0, b.length());
  }

  private static void writeBytes(
    final String s, final Charset charset, final OutputStream out) throws IOException {
    final ByteArrayBuffer b = encode(charset, s);
    writeBytes(b, out);
  }

  private static void writeBytes(
    final String s, final OutputStream out) throws IOException {
    final ByteArrayBuffer b = encode(MIME.DEFAULT_CHARSET, s);
    writeBytes(b, out);
  }

  protected static void writeField(
    final MinimalField field, final OutputStream out) throws IOException {
    writeBytes(field.getName(), out);
    writeBytes(FIELD_SEP, out);
    writeBytes(field.getBody(), out);
    writeBytes(CR_LF, out);
  }

  protected static void writeField(
    final MinimalField field, final Charset charset, final OutputStream out) throws IOException {
    writeBytes(field.getName(), charset, out);
    writeBytes(FIELD_SEP, out);
    writeBytes(field.getBody(), charset, out);
    writeBytes(CR_LF, out);
  }

  private static final ByteArrayBuffer FIELD_SEP = encode(MIME.DEFAULT_CHARSET, ": ");
  private static final ByteArrayBuffer CR_LF = encode(MIME.DEFAULT_CHARSET, "\r\n");
  private static final ByteArrayBuffer TWO_DASHES = encode(MIME.DEFAULT_CHARSET, "--");

  final Charset charset;
  final String boundary;

  /**
   * Creates an instance with the specified settings.
   *
   * @param charset  the character set to use. May be {@code null}, in which case {@link MIME#DEFAULT_CHARSET} - i.e. US-ASCII - is used.
   * @param boundary to use  - must not be {@code null}
   * @throws IllegalArgumentException if charset is null or boundary is null
   */
  public AbstractMultipartForm(final Charset charset, final String boundary) {
    super();
    Args.notNull(boundary, "Multipart boundary");
    this.charset = charset != null ? charset : MIME.DEFAULT_CHARSET;
    this.boundary = boundary;
  }

  public AbstractMultipartForm(final String boundary) {
    this(null, boundary);
  }

  public abstract List<FormBodyPart> getBodyParts();

  void doWriteTo(
    final OutputStream out,
    final boolean writeContent) throws IOException {

    final ByteArrayBuffer boundaryEncoded = encode(this.charset, this.boundary);
    for (final FormBodyPart part : getBodyParts()) {
      writeBytes(TWO_DASHES, out);
      writeBytes(boundaryEncoded, out);
      writeBytes(CR_LF, out);

      formatMultipartHeader(part, out);

      writeBytes(CR_LF, out);

      if (writeContent) {
        part.getBody().writeTo(out);
      }
      writeBytes(CR_LF, out);
    }
    writeBytes(TWO_DASHES, out);
    writeBytes(boundaryEncoded, out);
    writeBytes(TWO_DASHES, out);
    writeBytes(CR_LF, out);
  }

  /**
   * Write the multipart header fields; depends on the style.
   */
  protected abstract void formatMultipartHeader(
    final FormBodyPart part,
    final OutputStream out) throws IOException;

  /**
   * Writes out the content in the multipart/form encoding. This method
   * produces slightly different formatting depending on its compatibility
   * mode.
   */
  public void writeTo(final OutputStream out) throws IOException {
    doWriteTo(out, true);
  }

  /**
   * Determines the total length of the multipart content (content length of
   * individual parts plus that of extra elements required to delimit the parts
   * from one another). If any of the @{link BodyPart}s contained in this object
   * is of a streaming entity of unknown length the total length is also unknown.
   * <p>
   * This method buffers only a small amount of data in order to determine the
   * total length of the entire entity. The content of individual parts is not
   * buffered.
   * </p>
   *
   * @return total length of the multipart entity if known, {@code -1}
   * otherwise.
   */
  public long getTotalLength() {
    long contentLen = 0;
    for (final FormBodyPart part : getBodyParts()) {
      final ContentBody body = part.getBody();
      final long len = body.getContentLength();
      if (len >= 0) {
        contentLen += len;
      } else {
        return -1;
      }
    }
    final ByteArrayOutputStream out = new ByteArrayOutputStream();
    try {
      doWriteTo(out, false);
      final byte[] extra = out.toByteArray();
      return contentLen + extra.length;
    } catch (final IOException ex) {
      // Should never happen
      return -1;
    }
  }

}
